Анализ поведения пользователей мобильного приложения¶
Цель:
На базе информации о действиях, совершенными пользователями в мобильном приложении "Ненужные вещи", провести исследование и выяснить, как можно было бы улучшить приложение с точки зрения пользовательского опыта.
- Загрузка и предобработка данных
- импорт библиотек;
- загрузка датасетов;
- приведение названий полей к "змеиному стилю";
- приведение данных в ячейках к оптимальному типу;
- обработка пропусков;
- обработка дубликатов;
- преобразование "show_contacts" в "contacts_show";
- выводы и наблюдения.
- Исследовательский анализ данных
- оценить количество событий: показать количество событий по типу, отсортированных по убыванию, визуализировать, сделать выводы;
- оценить количество пользователей, визуализировать по типу источника, сделать выводы;
- определить период сбора данных, добавить столбец
date, визуализировать распределение активности пользователей по дням, сделать выводы; - выделить сессии пользователей через тайм-аут сессии;
- выводы и наблюдения.
- Основные вопросы исследования
- на базе сессий выделить сценарии действия пользователя в приложении;
- выделить сценарии, приводящие к действию
contacts_show("просмотр контактов"); - построить воронки по этим сценариям, сделать выводы;
- рассчитать относительную частоту событий в разрезе двух групп пользователей: те, кто совершал
contacts_show, и те, кто не совершал; - сравнить, сделать выводы.
- Проверка гипотез
- различие конверсии в просмотры контактов у двух групп пользователей: те, что делают
tips_showиtips_click, и те, что делают толькоtips_show, сделать выводы; - различие конверсии в просмотры контактов у двух групп пользователей: те, что просматривают фото (
photos_show), и те, что не просматривают; сделать выводы.
- различие конверсии в просмотры контактов у двух групп пользователей: те, что делают
- Общие выводы
Импорт библиотек¶
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import datetime as dt
from scipy import stats as st
import plotly.express as px
import math as mth
Загрузка датасетов¶
Датасет mobile_dataset.csv¶
data = pd.read_csv('https://code.s3.yandex.net/datasets/mobile_dataset.csv')
data.info()
display(data.head(10))
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event.time 74197 non-null object 1 event.name 74197 non-null object 2 user.id 74197 non-null object dtypes: object(3) memory usage: 1.7+ MB
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 5 | 2019-10-07 00:01:19.993624 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 6 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 7 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 8 | 2019-10-07 00:01:49.732803 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 9 | 2019-10-07 00:01:54.958298 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
Описание столбцов:
event.time - время события;
event.name - вид события;
user.id - уникальный идентификатор пользоователя.
Описание событий, столбец event.name:
advert_open - пользователь открыл карточку объявления после поиска (в приложении или из стороннего ресурса);
photos_show - пользователь посмотрел фотографию;
tips_show - увидел рекомендованные объявления;
tips_click - кликнул по рекомендованному объявлению;
contacts_show и show_contacts - посмотрел контакты продавца;
contacts_call - позвонил продавцу;
map - открыл карту объявлений;
search1 ... search7 - поиск по сайту;
favorites_add - добавил объявление в "Избранные".
Датасет mobile_sources.csv¶
source = pd.read_csv('https://code.s3.yandex.net/datasets/mobile_sources.csv')
source.info()
display(source.head(10))
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 userId 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB
| userId | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 | |
| 5 | 17f6b2db-2964-4d11-89d8-7e38d2cb4750 | yandex |
| 6 | 62aa104f-592d-4ccb-8226-2ba0e719ded5 | yandex |
| 7 | 57321726-5d66-4d51-84f4-c797c35dcf2b | |
| 8 | c2cf55c0-95f7-4269-896c-931d14deaab5 | |
| 9 | 48e614d6-fe03-40f7-bf9e-4c4f61c19f64 | yandex |
Описание столбцов:
userId - уникальный идентификатор пользователя;
source - источник, с которого пользователь установил приложение.
Выводы и наблюдения:
Данные не содержат пропусков. Названия некоторых столбцов нужно привести к "змеиному" стилю. Данные в столбце event.time привести к типу datetime.
Переименование столбцов¶
data.columns = ['event_time','event_name','user_id']
source.columns = ['user_id','source']
Приведение значений в столбце event_time к типу datetime¶
data['event_time'] = pd.to_datetime(data['event_time'])
data.info()
display(data.head(10))
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 1.7+ MB
| event_time | event_name | user_id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 5 | 2019-10-07 00:01:19.993624 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 6 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 7 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 8 | 2019-10-07 00:01:49.732803 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 9 | 2019-10-07 00:01:54.958298 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
Обработка дубликатов¶
Явные дубликаты¶
print('Количество дубликатов в датасете data: ',data.duplicated().sum())
print('Количество дубликатов в датасете source: ',source.duplicated().sum())
Количество дубликатов в датасете data: 0 Количество дубликатов в датасете source: 0
Неявные дубликаты¶
print(source['source'].unique())
['other' 'yandex' 'google']
В датасете source неявных дубликатов нет.
print(data['event_name'].unique())
['advert_open' 'tips_show' 'map' 'contacts_show' 'search_4' 'search_5' 'tips_click' 'photos_show' 'search_1' 'search_2' 'search_3' 'favorites_add' 'contacts_call' 'search_6' 'search_7' 'show_contacts']
Два значения, contacts_show и show_contacts, обозначающие одно и тоже событие, возникли, вероятно, при сборе информации. Поставим везде contacts_show и снова проверим на явные дубликаты.
data['event_name'] = data['event_name'].str.replace('show_contacts','contacts_show')
# проверим результат
print(data['event_name'].unique())
['advert_open' 'tips_show' 'map' 'contacts_show' 'search_4' 'search_5' 'tips_click' 'photos_show' 'search_1' 'search_2' 'search_3' 'favorites_add' 'contacts_call' 'search_6' 'search_7']
# проверим на явные дубликаты
print('Количество дубликатов в датасете data: ',data.duplicated().sum())
Количество дубликатов в датасете data: 0
Итоги раздела¶
Мы загрузили и изучили два датасета с информацией о действиях, совершенных пользователями в приложении.
Проверили на наличие пропусков и дубликатов. Таковых не оказалось.
Привели названия столбцов в датасетах к "змеиному" стилю.
Оптимизировали тип данных в столбце event_time датасета data.
Оценка количества событий, визуализация количества событий по типу¶
event_name_n = data.groupby('event_name')['user_id'].count().sort_values(ascending=False)
display(event_name_n)
event_name tips_show 40055 photos_show 10012 advert_open 6164 contacts_show 4529 map 3881 search_1 3506 favorites_add 1417 search_5 1049 tips_click 814 search_4 701 contacts_call 541 search_3 522 search_6 460 search_2 324 search_7 222 Name: user_id, dtype: int64
plt.figure(figsize=(14,8), dpi=100)
sns.barplot(x=event_name_n, y=event_name_n.index, orient='h', color = '#5566ca')
plt.title('Количество событий по типу', fontsize=14)
plt.xlabel('Количество событий')
plt.ylabel('Событие')
plt.grid();
print(data.query('event_name == "tips_show"')['user_id'].nunique())
2801
Оценка количества пользователей, визуализация по типу источника¶
print('Количество уникальных пользователей: ', data['user_id'].nunique())
Количество уникальных пользователей: 4293
Количество уникальных пользователей в двух датасетах совпадает - 4293.
Посмотрим, каким источником они пользовались для установки приложения.
source_type_n = source.groupby('source').count().sort_values(by='user_id',ascending=False)
display(source_type_n)
| user_id | |
|---|---|
| source | |
| yandex | 1934 |
| other | 1230 |
| 1129 |
plt.figure(figsize=(6,5), dpi=100)
sns.barplot(y=source_type_n['user_id'], x=source_type_n.index)
plt.title('Количество источников по типу', fontsize=12)
plt.xlabel('Источник')
plt.ylabel('Количество источников');
У Яндекса минимум в полтора раза больше скачиваний приложения "Ненужные вещи", чем у других источников.
Определение периода сбора данных, добавление столбца date, визуализация активности пользователей по дням¶
print('Начало наблюдений: ', data['event_time'].min())
print('Завершение наблюдений: ', data['event_time'].max())
Начало наблюдений: 2019-10-07 00:00:00.431357 Завершение наблюдений: 2019-11-03 23:58:12.532487
Имеем данные за 28 полных дней, ровно четыре недели, с понедельника по воскресенье.
Добавим в датасет столбец с датой:
data['date'] = data['event_time'].dt.date
display(data.head())
data.info()
| event_time | event_name | user_id | date | |
|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | 2019-10-07 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | 2019-10-07 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object 3 date 74197 non-null object dtypes: datetime64[ns](1), object(3) memory usage: 2.3+ MB
Проверим полноту данных, визуализируем количество событий по дням, а заодно количество целевых событий contacts_show по дням.
data_date_n = data.groupby(by=['date']).agg({'event_name':'count'}).reset_index()
contacts_show_n = (data
.query('event_name == "contacts_show"')
.groupby(by=['date']).agg({'event_name':'count'})
.reset_index()
)
plt.figure(figsize=(15,8), dpi=100)
sns.barplot(x='date', y='event_name', data=data_date_n, color='#5566ca',label='все события')
sns.barplot(x='date', y='event_name', data=contacts_show_n, color='lightgreen',label='contacts_show')
plt.title('Количество событий по дням', fontsize=14)
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.xticks(rotation=30)
plt.axhline(data_date_n['event_name'].mean(), color='black', linestyle='--',label='среднее количество событий')
plt.axhline(contacts_show_n['event_name'].mean(), color='darkgreen', linestyle='--',label='среднее количество событий contacts_show')
plt.legend()
plt.show();
Количество событий колеблется около 2650. Тренд за месяц определить нельзя, но, кажется, в первую неделю событий было меньше.
Целевое действие contacts_show совершается также с равной, в среднем, интенсивностью, не считая первой недели, где интенсивность явно меньше.
Выделение сессий¶
Для того, чтобы в дальнейшем найти все пользовательские сценарии действий, сначала разобьем имеющиеся события на последовательности - сессии.
Перед поиском сессий отсортируем датасет:
data_sorted = data.sort_values(by=['user_id', 'event_time'])
display(data_sorted.head())
| event_time | event_name | user_id | date | |
|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
Добавим столбец с разностью времени в секундах между настоящим и предыдущим событиями:
data_sorted['time_diff'] = data_sorted['event_time'].diff().dt.total_seconds()
display(data_sorted.head(60))
data_sorted.info()
| event_time | event_name | user_id | date | time_diff | |
|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | NaN |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 4.506355e+01 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 3.466958e+01 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.350130e+02 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.301820e+02 |
| 831 | 2019-10-07 13:45:43.212340 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.229484e+01 |
| 832 | 2019-10-07 13:46:31.033718 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 4.782138e+01 |
| 836 | 2019-10-07 13:47:32.860234 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 6.182652e+01 |
| 839 | 2019-10-07 13:49:41.716617 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.288564e+02 |
| 6541 | 2019-10-09 18:33:55.577963 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 1.898539e+05 |
| 6546 | 2019-10-09 18:35:28.260975 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 9.268301e+01 |
| 6565 | 2019-10-09 18:40:28.738785 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 3.004778e+02 |
| 6566 | 2019-10-09 18:42:22.963948 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 1.142252e+02 |
| 36412 | 2019-10-21 19:52:30.778932 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.041008e+06 |
| 36416 | 2019-10-21 19:53:17.165009 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 4.638608e+01 |
| 36419 | 2019-10-21 19:53:38.767230 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2.160222e+01 |
| 36421 | 2019-10-21 19:54:45.009859 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 6.624263e+01 |
| 36423 | 2019-10-21 19:54:56.854811 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.184495e+01 |
| 36430 | 2019-10-21 19:56:49.417415 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.125626e+02 |
| 36435 | 2019-10-21 19:57:21.124551 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3.170714e+01 |
| 36437 | 2019-10-21 19:57:49.029206 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2.790466e+01 |
| 36447 | 2019-10-21 20:00:00.438922 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.314097e+02 |
| 36454 | 2019-10-21 20:01:16.839387 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 7.640047e+01 |
| 36459 | 2019-10-21 20:01:52.099777 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3.526039e+01 |
| 36473 | 2019-10-21 20:05:04.173348 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.920736e+02 |
| 36481 | 2019-10-21 20:06:47.035115 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.028618e+02 |
| 36486 | 2019-10-21 20:07:30.051028 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 4.301591e+01 |
| 37556 | 2019-10-22 11:18:14.635436 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 5.464458e+04 |
| 37559 | 2019-10-22 11:19:10.529462 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 5.589403e+01 |
| 37566 | 2019-10-22 11:20:12.571696 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 6.204223e+01 |
| 37571 | 2019-10-22 11:21:30.964099 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 7.839240e+01 |
| 37581 | 2019-10-22 11:25:33.508919 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 2.425448e+02 |
| 37591 | 2019-10-22 11:28:05.165918 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 1.516570e+02 |
| 37601 | 2019-10-22 11:30:05.522265 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 1.203563e+02 |
| 37607 | 2019-10-22 11:30:52.807203 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 4.728494e+01 |
| 31632 | 2019-10-19 21:34:33.849769 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | -2.229790e+05 |
| 31636 | 2019-10-19 21:35:19.296599 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 4.544683e+01 |
| 31640 | 2019-10-19 21:36:44.344691 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 8.504809e+01 |
| 31655 | 2019-10-19 21:40:38.990477 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 2.346458e+02 |
| 31659 | 2019-10-19 21:42:13.837523 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 9.484705e+01 |
| 31670 | 2019-10-19 21:44:55.589731 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.617522e+02 |
| 31685 | 2019-10-19 21:46:52.541309 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.169516e+02 |
| 31730 | 2019-10-19 21:58:00.109019 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 6.675677e+02 |
| 31737 | 2019-10-19 21:59:54.637098 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.145281e+02 |
| 33482 | 2019-10-20 18:49:24.115634 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 7.496948e+04 |
| 33498 | 2019-10-20 18:59:22.541082 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 5.984254e+02 |
| 33510 | 2019-10-20 19:03:02.030004 | favorites_add | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 2.194889e+02 |
| 33514 | 2019-10-20 19:04:16.149734 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 7.411973e+01 |
| 33523 | 2019-10-20 19:09:56.162564 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 3.400128e+02 |
| 33528 | 2019-10-20 19:11:47.344296 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.111817e+02 |
| 33533 | 2019-10-20 19:17:18.659799 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 3.313155e+02 |
| 33534 | 2019-10-20 19:17:24.887762 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 6.227963e+00 |
| 33537 | 2019-10-20 19:18:54.738758 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 8.985100e+01 |
| 33540 | 2019-10-20 19:20:41.699609 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.069609e+02 |
| 33544 | 2019-10-20 19:23:11.839947 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.501403e+02 |
| 33545 | 2019-10-20 19:23:14.236973 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 2.397026e+00 |
| 33565 | 2019-10-20 19:30:31.912891 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.376759e+02 |
| 33566 | 2019-10-20 19:30:36.096917 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.184026e+00 |
| 33601 | 2019-10-20 19:57:15.652784 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.599556e+03 |
| 33615 | 2019-10-20 20:04:16.954396 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.213016e+02 |
<class 'pandas.core.frame.DataFrame'> Int64Index: 74197 entries, 805 to 72689 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object 3 date 74197 non-null object 4 time_diff 74196 non-null float64 dtypes: datetime64[ns](1), float64(1), object(3) memory usage: 3.4+ MB
Построим распределение разницы времени между соседними событиями в сессии.
Ограничим значения нулем слева и одним часом, т.е. 3600 секундами, справа:
plt.figure(figsize=(8,5), dpi=100)
plt.hist(data_sorted.query('time_diff > 0 and time_diff < 3600')['time_diff'], bins=100)
plt.title('Распределение разницы времени между соседними событиями в сессии', fontsize=10)
plt.xlabel('Разница времени')
plt.ylabel('Количество наблюдений');
Для более точного определения тайм-аута сессии найдем 95-й перцентиль среди значений столбца time_diff и будем считать его порогом, выше которого будут находиться нехарактерные для внутрисессионных интервалов значения разницы времени:
session_timeout = np.percentile(data_sorted.query('time_diff > 0 and time_diff < 3600')['time_diff'],95)
print('Тайм-аут сессии = ', session_timeout.round(1), 'секунды')
Тайм-аут сессии = 579.6 секунды
Т.е. чуть больше девяти с половиной минут.
Добавим в датасет столбец с номером сессии, к которой относится каждое событие:
g = (data_sorted.groupby('user_id')['event_time'].diff().dt.total_seconds() > session_timeout).cumsum()
data_sorted['session_id'] = data_sorted.groupby(['user_id', g], sort=False).ngroup() + 1
display(data_sorted.head(60),data_sorted.tail(5))
| event_time | event_name | user_id | date | time_diff | session_id | |
|---|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | NaN | 1 |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 4.506355e+01 | 1 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 3.466958e+01 | 1 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.350130e+02 | 1 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.301820e+02 | 1 |
| 831 | 2019-10-07 13:45:43.212340 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.229484e+01 | 1 |
| 832 | 2019-10-07 13:46:31.033718 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 4.782138e+01 | 1 |
| 836 | 2019-10-07 13:47:32.860234 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 6.182652e+01 | 1 |
| 839 | 2019-10-07 13:49:41.716617 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1.288564e+02 | 1 |
| 6541 | 2019-10-09 18:33:55.577963 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 1.898539e+05 | 2 |
| 6546 | 2019-10-09 18:35:28.260975 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 9.268301e+01 | 2 |
| 6565 | 2019-10-09 18:40:28.738785 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 3.004778e+02 | 2 |
| 6566 | 2019-10-09 18:42:22.963948 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 1.142252e+02 | 2 |
| 36412 | 2019-10-21 19:52:30.778932 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.041008e+06 | 3 |
| 36416 | 2019-10-21 19:53:17.165009 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 4.638608e+01 | 3 |
| 36419 | 2019-10-21 19:53:38.767230 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2.160222e+01 | 3 |
| 36421 | 2019-10-21 19:54:45.009859 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 6.624263e+01 | 3 |
| 36423 | 2019-10-21 19:54:56.854811 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.184495e+01 | 3 |
| 36430 | 2019-10-21 19:56:49.417415 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.125626e+02 | 3 |
| 36435 | 2019-10-21 19:57:21.124551 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3.170714e+01 | 3 |
| 36437 | 2019-10-21 19:57:49.029206 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 2.790466e+01 | 3 |
| 36447 | 2019-10-21 20:00:00.438922 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.314097e+02 | 3 |
| 36454 | 2019-10-21 20:01:16.839387 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 7.640047e+01 | 3 |
| 36459 | 2019-10-21 20:01:52.099777 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3.526039e+01 | 3 |
| 36473 | 2019-10-21 20:05:04.173348 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.920736e+02 | 3 |
| 36481 | 2019-10-21 20:06:47.035115 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 1.028618e+02 | 3 |
| 36486 | 2019-10-21 20:07:30.051028 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 4.301591e+01 | 3 |
| 37556 | 2019-10-22 11:18:14.635436 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 5.464458e+04 | 4 |
| 37559 | 2019-10-22 11:19:10.529462 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 5.589403e+01 | 4 |
| 37566 | 2019-10-22 11:20:12.571696 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 6.204223e+01 | 4 |
| 37571 | 2019-10-22 11:21:30.964099 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 7.839240e+01 | 4 |
| 37581 | 2019-10-22 11:25:33.508919 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 2.425448e+02 | 4 |
| 37591 | 2019-10-22 11:28:05.165918 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 1.516570e+02 | 4 |
| 37601 | 2019-10-22 11:30:05.522265 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 1.203563e+02 | 4 |
| 37607 | 2019-10-22 11:30:52.807203 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 | 4.728494e+01 | 4 |
| 31632 | 2019-10-19 21:34:33.849769 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | -2.229790e+05 | 5 |
| 31636 | 2019-10-19 21:35:19.296599 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 4.544683e+01 | 5 |
| 31640 | 2019-10-19 21:36:44.344691 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 8.504809e+01 | 5 |
| 31655 | 2019-10-19 21:40:38.990477 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 2.346458e+02 | 5 |
| 31659 | 2019-10-19 21:42:13.837523 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 9.484705e+01 | 5 |
| 31670 | 2019-10-19 21:44:55.589731 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.617522e+02 | 5 |
| 31685 | 2019-10-19 21:46:52.541309 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.169516e+02 | 5 |
| 31730 | 2019-10-19 21:58:00.109019 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 6.675677e+02 | 6 |
| 31737 | 2019-10-19 21:59:54.637098 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 | 1.145281e+02 | 6 |
| 33482 | 2019-10-20 18:49:24.115634 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 7.496948e+04 | 7 |
| 33498 | 2019-10-20 18:59:22.541082 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 5.984254e+02 | 8 |
| 33510 | 2019-10-20 19:03:02.030004 | favorites_add | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 2.194889e+02 | 8 |
| 33514 | 2019-10-20 19:04:16.149734 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 7.411973e+01 | 8 |
| 33523 | 2019-10-20 19:09:56.162564 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 3.400128e+02 | 8 |
| 33528 | 2019-10-20 19:11:47.344296 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.111817e+02 | 8 |
| 33533 | 2019-10-20 19:17:18.659799 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 3.313155e+02 | 8 |
| 33534 | 2019-10-20 19:17:24.887762 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 6.227963e+00 | 8 |
| 33537 | 2019-10-20 19:18:54.738758 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 8.985100e+01 | 8 |
| 33540 | 2019-10-20 19:20:41.699609 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.069609e+02 | 8 |
| 33544 | 2019-10-20 19:23:11.839947 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.501403e+02 | 8 |
| 33545 | 2019-10-20 19:23:14.236973 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 2.397026e+00 | 8 |
| 33565 | 2019-10-20 19:30:31.912891 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.376759e+02 | 8 |
| 33566 | 2019-10-20 19:30:36.096917 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.184026e+00 | 8 |
| 33601 | 2019-10-20 19:57:15.652784 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 1.599556e+03 | 9 |
| 33615 | 2019-10-20 20:04:16.954396 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 | 4.213016e+02 | 9 |
| event_time | event_name | user_id | date | time_diff | session_id | |
|---|---|---|---|---|---|---|
| 72584 | 2019-11-03 15:51:23.959572 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 27.886483 | 12801 |
| 72589 | 2019-11-03 15:51:57.899997 | contacts_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 33.940425 | 12801 |
| 72684 | 2019-11-03 16:07:40.932077 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 943.032080 | 12802 |
| 72688 | 2019-11-03 16:08:18.202734 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 37.270657 | 12802 |
| 72689 | 2019-11-03 16:08:25.388712 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 | 7.185978 | 12802 |
Всего 12802 сессии.
Итоги раздела:¶
В этом разделе проекта мы проводили исследовательский анализ входных данных.
- Оценили общее количество событий по их видам:
- первое место, 40000 из 74000 - главная страница, показ рекомендованных объявлений, - с учетом количества пользователей в среднем по 10 событий на пользователя;
- просмотр фотографий - второе место - 10000 событий;
- целевое событие,
contacts_show, на четвертом месте, 4500 событий, это чуть больше, чем по одному такому событию на пользователя в среднем.
- Нашли, как пользователи распределены по источникам установки приложения:
Яндекс с полуторакратным отрывом от примерно равных результатов у Гугл и других источников. - Определили период исследования, визуализировали активность пользователей по дням:
- период исследования с 07.10.2019 по 03.11.2019 - ровно четыре недели с понедельника по воскресенье;
- ежедневное количество событий за этот период колеблется от 1800 до 3300, среднее значение - 2650;
- целевое событие
contacts_showежедневно случается, в среднем 170 раз; - на визуализации заметно, что в первую неделю наблюдений активность пользователей была чуть меньше.
- Выделили сессии из общего потока событий:
всего выделено 12802 сессии, т.е. в среднем чуть больше, чем по три сессии на пользователя.
Выделение пользовательских сценариев¶
Определим какие последовательности событий присутствуют в сессиях:
# объединим все действия поиска в один 'search'
# функция для замены значения `search..`
def change_search(string):
if 'search' in string:
return 'search'
else: return string
# применим ф-ию для столбца `event_name`
data_sorted['event_name'] = data_sorted['event_name'].apply(change_search)
# проверим результат
print(data_sorted['event_name'].unique())
# сгруппируем по номеру сессии
sessions = data_sorted.groupby('session_id',as_index=False).agg({'event_name':'unique','user_id':'first','event_time':'min'})
display(sessions.head(10))
['tips_show' 'map' 'search' 'photos_show' 'favorites_add' 'contacts_show' 'contacts_call' 'advert_open' 'tips_click']
| session_id | event_name | user_id | event_time | |
|---|---|---|---|---|
| 0 | 1 | [tips_show] | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:45.989359 |
| 1 | 2 | [map, tips_show] | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 18:33:55.577963 |
| 2 | 3 | [tips_show, map] | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 19:52:30.778932 |
| 3 | 4 | [map, tips_show] | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-22 11:18:14.635436 |
| 4 | 5 | [search, photos_show] | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:33.849769 |
| 5 | 6 | [search, photos_show] | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:58:00.109019 |
| 6 | 7 | [search] | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 18:49:24.115634 |
| 7 | 8 | [photos_show, favorites_add, search, contacts_... | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 18:59:22.541082 |
| 8 | 9 | [photos_show, contacts_show] | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 19:57:15.652784 |
| 9 | 10 | [photos_show, advert_open] | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-24 10:50:40.219833 |
Получим таблицу со всевозможными сценариями и их количеством, отсортируем по убыванию:
# преобразуем ndarray в tuple, чтобы можно было сгруппировать по сценарию
sessions['event_name'] = sessions['event_name'].apply(tuple)
# сгруппируем по `event_name` и выведем результат
session_scrypts = sessions.groupby('event_name',as_index=False)['session_id'].count()
display(session_scrypts.sort_values('session_id',ascending=False).head(10))
print('Всего сценариев:',len(session_scrypts))
| event_name | session_id | |
|---|---|---|
| 279 | (tips_show,) | 3775 |
| 166 | (photos_show,) | 1716 |
| 196 | (search,) | 991 |
| 148 | (map, tips_show) | 590 |
| 237 | (search, photos_show) | 513 |
| 289 | (tips_show, contacts_show) | 451 |
| 249 | (search, tips_show) | 415 |
| 44 | (contacts_show,) | 318 |
| 312 | (tips_show, map) | 231 |
| 28 | (advert_open, tips_show) | 212 |
Всего сценариев: 354
Выделение целевых сценариев¶
Выделим из общего количества только те сценарии, где есть целевое действие contacts_show, и оно последнее:
# функция для проверки сценария на целевой
def target(list):
if list[-1] == 'contacts_show': # если последний элемент списка равен "contacts_show", возвращаем True, нет - False
return True
else: return False
# получаем датафрейм с целевыми сценариями, применяя ф-ию `target`
target_scrypts = session_scrypts[session_scrypts['event_name'].apply(target)]
display(target_scrypts.sort_values('session_id', ascending=False).head(20))
print('Всего сценариев:',len(target_scrypts))
| event_name | session_id | |
|---|---|---|
| 289 | (tips_show, contacts_show) | 451 |
| 44 | (contacts_show,) | 318 |
| 171 | (photos_show, contacts_show) | 88 |
| 155 | (map, tips_show, contacts_show) | 87 |
| 203 | (search, contacts_show) | 58 |
| 240 | (search, photos_show, contacts_show) | 36 |
| 251 | (search, tips_show, contacts_show) | 28 |
| 320 | (tips_show, map, contacts_show) | 22 |
| 346 | (tips_show, tips_click, contacts_show) | 20 |
| 1 | (advert_open, contacts_show) | 16 |
| 124 | (map, advert_open, tips_show, contacts_show) | 13 |
| 129 | (map, contacts_show) | 12 |
| 29 | (advert_open, tips_show, contacts_show) | 9 |
| 232 | (search, map, tips_show, contacts_show) | 8 |
| 189 | (photos_show, search, contacts_show) | 7 |
| 94 | (favorites_add, contacts_show) | 6 |
| 281 | (tips_show, advert_open, contacts_show) | 6 |
| 161 | (map, tips_show, favorites_add, contacts_show) | 5 |
| 226 | (search, map, advert_open, tips_show, contacts... | 5 |
| 117 | (map, advert_open, contacts_show) | 4 |
Всего сценариев: 54
Воронки событий наиболее популярных сценариев¶
Визуализируем воронки событий по следующим выбранным сценариям, где целевым событием является contacts_show:
'tips_show' -> 'contacts_show'
'photos_show' -> 'contacts_show'
'map' -> 'tips_show' -> 'contacts_show'
'search' -> 'contacts_show'
# создадим функцию для получения данных для воронки и ее отрисовки:
def show_funnel(events): # на входе - список из названий событий - сценарий
users = [] # список пользователей
start = True # признак начала, равен True перед выполнением первого цикла, чтобы не сравнивать список пользователей с пустым списком
funnel = pd.DataFrame(columns=['users_amount','total_percent'],index=events) # датасет с данными для построения воронки
# для каждого события из входного сценария:
for e in events:
# находим список уников для события, текущий список
users_c = list(data_sorted.query('event_name == @e')['user_id'].unique())
# проверка начала
if not start: # для каждого пользователя в текущем списке проверяем наличие этого пользователя
temp_list = [] # в предыдущем списке пользователей и, если обнаружили, добавляем во временный список
for u in users_c:
if u in users:
temp_list.append(u)
users_c = temp_list
start = False # устанавливаем тэг начала в значение False
users = users_c # обновляем список пользователей
funnel.loc[e]['users_amount'] = len(users_c) # и заносим количество уников в датасет воронки
funnel.loc[e]['total_percent'] = round(100*(len(users_c)/len(source)),2) # добавляем процент от общего количества
funnel['conversion'] = funnel['total_percent'].shift() # новый столбец "конверсия" создается сдвигом столбца с процентами на одну строку вниз
funnel.loc[events[0]]['conversion'] = funnel.loc[events[0]]['total_percent'] # равняем значения, чтобы потом получить в этой ячейке 100%
funnel['conversion'] = 100*funnel['total_percent'] / funnel['conversion'] # считаем столбец конверсии
# вывод таблицы
# display(funnel)
# вывод визуализации
fig = px.funnel(funnel, y='conversion', x=funnel.index, title = 'Воронка конверсии событий для сценария '+str(events))
fig.show()
# применяем фунцию draw_funnel
show_funnel(['tips_show', 'contacts_show'])
show_funnel(['photos_show', 'contacts_show'])
show_funnel(['map','tips_show', 'contacts_show'])
show_funnel(['search', 'contacts_show'])
Так или иначе, разными путями, но пользователи пытаются узнать контакты продавцов. По представленным сценариям сложно определить, какие из них точно лучше, чем другие, но бОльшая конверсия в целевое действие - у сценария [photos_show -> contacts_show].
Самая слабая конверсия в последовательностях tips_show -> contacts_show и search -> contacts_show. Возможно, плохо работает поиск, показывая пользователям не совсем тот продукт, который они искали.
Расчет относительной частоты событий¶
Оценим, какие действия чаще совершают те пользователи, которые просматривают контакты.
Рассчитаем относительную частоту событий для двух групп пользователей:
- те, которые совершали
contacts_show; - и те, которые не совершали
contacts_show.
# список пользователей, совершавших `contacts_show`
target_users = data_sorted.query('event_name == "contacts_show"')['user_id'].unique()
print('Количество пользователей, совершавших `contacts_show`:'
,len(target_users), 'из', len(source),', т.е.'
,round(100*len(target_users)/len(source),2),'%')
Количество пользователей, совершавших `contacts_show`: 981 из 4293 , т.е. 22.85 %
# относительная частота событий целевых пользователей
target_users_nevents = (data_sorted
.query('user_id in @target_users')
.groupby('event_name',as_index=False)['user_id'].count()
)
# удалим события `contacts_show` и `contacts_call`, т.к. пользователи из другой группы их не совершают
target_users_nevents = target_users_nevents.query('event_name != "contacts_show" and event_name != "contacts_call"')
# посчитаем столбец с долей событий в процентах
target_users_nevents['percent'] = 100*target_users_nevents['user_id']/target_users_nevents['user_id'].sum()
# отсортируем по убыванию величины доли
target_users_nevents = target_users_nevents.sort_values('percent',ascending=0)
display(target_users_nevents)
| event_name | user_id | percent | |
|---|---|---|---|
| 8 | tips_show | 12768 | 57.703258 |
| 5 | photos_show | 3828 | 17.300131 |
| 6 | search | 2084 | 9.418358 |
| 0 | advert_open | 1589 | 7.181272 |
| 4 | map | 1101 | 4.975821 |
| 3 | favorites_add | 424 | 1.916211 |
| 7 | tips_click | 333 | 1.504949 |
# относительная частота событий нецелевых пользователей
nontarget_users_nevents = (data_sorted
.query('user_id not in @target_users')
.groupby('event_name',as_index=False)['user_id'].count()
)
nontarget_users_nevents['percent'] = 100*nontarget_users_nevents['user_id']/nontarget_users_nevents['user_id'].sum()
nontarget_users_nevents = nontarget_users_nevents.sort_values('percent',ascending=0)
display(nontarget_users_nevents)
| event_name | user_id | percent | |
|---|---|---|---|
| 6 | tips_show | 27287 | 58.057447 |
| 3 | photos_show | 6184 | 13.157447 |
| 4 | search | 4700 | 10.000000 |
| 0 | advert_open | 4575 | 9.734043 |
| 2 | map | 2780 | 5.914894 |
| 1 | favorites_add | 993 | 2.112766 |
| 5 | tips_click | 481 | 1.023404 |
# объединяем в один датафрейм
target_users_nevents['tag'] = 'пользователи, совершавшие `contacts_show`'
nontarget_users_nevents['tag'] = 'пользователи, не совершавшие `contacts_show`'
users_nevents = pd.concat([target_users_nevents,nontarget_users_nevents])
users_nevents.columns = ['event_name','amount','percent','tag']
display(users_nevents)
| event_name | amount | percent | tag | |
|---|---|---|---|---|
| 8 | tips_show | 12768 | 57.703258 | пользователи, совершавшие `contacts_show` |
| 5 | photos_show | 3828 | 17.300131 | пользователи, совершавшие `contacts_show` |
| 6 | search | 2084 | 9.418358 | пользователи, совершавшие `contacts_show` |
| 0 | advert_open | 1589 | 7.181272 | пользователи, совершавшие `contacts_show` |
| 4 | map | 1101 | 4.975821 | пользователи, совершавшие `contacts_show` |
| 3 | favorites_add | 424 | 1.916211 | пользователи, совершавшие `contacts_show` |
| 7 | tips_click | 333 | 1.504949 | пользователи, совершавшие `contacts_show` |
| 6 | tips_show | 27287 | 58.057447 | пользователи, не совершавшие `contacts_show` |
| 3 | photos_show | 6184 | 13.157447 | пользователи, не совершавшие `contacts_show` |
| 4 | search | 4700 | 10.000000 | пользователи, не совершавшие `contacts_show` |
| 0 | advert_open | 4575 | 9.734043 | пользователи, не совершавшие `contacts_show` |
| 2 | map | 2780 | 5.914894 | пользователи, не совершавшие `contacts_show` |
| 1 | favorites_add | 993 | 2.112766 | пользователи, не совершавшие `contacts_show` |
| 5 | tips_click | 481 | 1.023404 | пользователи, не совершавшие `contacts_show` |
Сравнить долю тех или иных событий будет проще на графике:
plt.figure(figsize=(12,6), dpi=100)
sns.barplot(x='event_name', y='percent', data=users_nevents, hue='tag')
plt.title('Относительная частота событий', fontsize=14)
plt.xlabel('События')
plt.ylabel('Процент от общего количества событий в группе')
plt.legend()
plt.show();
Из графика видно, что доля события photos_show и доля события tips_click у пользователей, совершавших contacts_show, в 1,3 и в 1,5 раза соответственно выше, чем у остальных пользователей. Относительная частота событий advert_open, search и map, наоборот, ниже. Можно сделать вывод, что решение о покупке пользователь делает преимущественно на основе просмотра фотографий.
Также мы выяснили, что действие contacts_call совершают только те, кто совершил contacts_show.
Итоги раздела:¶
В этом разделе мы подробно рассматривали влияние других событий на целевое, contacts_show.
- Из ранее полученных сессий мы выделили сценарии - последовательности событий. Всего определили 354 различных сценария.
- Из сценариев выделили те, которые приводят в итоге к целевому событию, а также рассчитали их количество. Получилось 54 целевых сценария.
- По наиболее частым сценариям построили воронки конверсии, наиболее эффективным из выбранных оказался переход от
photos_showкcontacts_show. - Определили и визуализировали относительную частоту событий для двух групп пользователей: одна группа - те, кто совершал
contacts_show, другая - те, кто не совершал. Выяснилось, что пользователи, совершавшие целевое действие, чаще просматривают фотографии, чем остальные. Что согласуется с предыдущим пунктом. Также они чаще кликают рекомендованные объявления.
Гипотеза о различии конверсии в просмотр контактов у двух групп пользователей: те, которые совершают действия tips_show и tips_click, и те, которые совершают только tips_show.¶
- Нулевой гипотезой, Но, будет гипотеза о равенстве конверсии между "кликерами" (те, кто совершают
tips_click) и "некликерами" (те, кто не кликают по рекомендациям) в просмотр контактов (contacts_show). - Альтернативной гипотезой, На, будет гипотеза о различии в конверсии между этими группами.
- Тест будет двусторонний, т.к. в условии задачи не указано, в какую сторону должно быть различие.
- Порог статистической значимости
alphaвозьмем равным 0,05. - Проверять статистическое различие между выборками мы будем путем сравнения их долей в общей совокупности. Для этого лучше всего подходит
z-тест. Перед применением z-теста проведем несколько подготовительных вычислений, найдем количества пользователей по группам для каждого события и найдем их доли.
Найдем количество уников для кликеров и некликеров:
# дафтафрейм с пользователем, событием и количеством этого события для пользователя
user_event_n = data.groupby(['user_id', 'event_name'],as_index=0).count()
display(user_event_n.head())
| user_id | event_name | event_time | date | |
|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | map | 6 | 6 |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | tips_show | 29 | 29 |
| 2 | 00157779-810c-4498-9e05-a1e9e3cedf93 | advert_open | 2 | 2 |
| 3 | 00157779-810c-4498-9e05-a1e9e3cedf93 | contacts_call | 5 | 5 |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | contacts_show | 11 | 11 |
# количество кликеров
both_events = (user_event_n # датафрейм всех пользователей, кто совершал хотя бы одно из событий tips_show или tips_click
.query('event_name == "tips_show" or event_name == "tips_click"')
.groupby('user_id',as_index=0).count()
)
clickers_n = both_events.query('event_name == 2')['user_id'].count() # выбор и подсчет пользователей с обеими событиями
print('Количество кликеров:', clickers_n)
Количество кликеров: 297
# количество некликеров
nonclickers_and_clickers = user_event_n.query('event_name == "tips_show"') # датафрейм всех, кто совершал tips_show (содержит также и кликеров)
nonclickers_n = len(nonclickers_and_clickers) - clickers_n # вычитаем кликеров из общего числа
print('Количество некликеров:', nonclickers_n)
Количество некликеров: 2504
Найдем количество пользователей, из кликеров и некликеров, которые совершали contacts_show
# количество целевых кликеров
three_events = (user_event_n
.query('event_name == "tips_show" or event_name == "tips_click" or event_name == "contacts_show"')
.groupby('user_id',as_index=0).count()
)
target_clickers_n = three_events.query('event_name == 3')['user_id'].count()
print('Количество целевых кликеров:', target_clickers_n)
Количество целевых кликеров: 91
# количество целевых некликеров
target_nonclickers_and_clickers = (user_event_n
.query('event_name == "tips_show" or event_name == "contacts_show"')
.groupby('user_id',as_index=0).count()
)
target_nonclickers_n = target_nonclickers_and_clickers.query('event_name == 2')['user_id'].count() - target_clickers_n
print('Количество целевых некликеров:', target_nonclickers_n)
Количество целевых некликеров: 425
Проверим гипотезу:
# функция для проверки z-теста
def z_test(target_users_n, users_n, target_nonusers_n, nonusers_n):
alpha = 0.05 # порог статистической значимости
p1 = target_users_n / users_n # конверсия первой группы
p2 = target_nonusers_n / nonusers_n # # конверсия второй группы
p_combined = (target_users_n + target_nonusers_n) / (users_n + nonusers_n) # конверсия общая
# расчет z-критерия
z_value = (p1 - p2) / mth.sqrt(p_combined * (1 - p_combined) * (1/users_n + 1/nonusers_n))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(loc=0, scale=1)
# считаем p-value
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу')
else:
print('Не получилось отвергнуть нулевую гипотезу')
# проверка гипотезы
z_test(target_clickers_n, clickers_n, target_nonclickers_n, nonclickers_n)
p-значение: 9.218316554537864e-09 Отвергаем нулевую гипотезу
# конверсия групп
print('Конверсия целевой группы: ', 100*target_clickers_n / clickers_n, '%'
, '\nКонверсия нецелевой группы: ', 100*target_nonclickers_n / nonclickers_n, '%')
# различие конверсий
print('\nОтношение конверсий: ',(target_clickers_n/clickers_n)/(target_nonclickers_n/nonclickers_n))
Конверсия целевой группы: 30.63973063973064 % Конверсия нецелевой группы: 16.972843450479232 % Отношение конверсий: 1.8052208358090711
Проверка показывает, что между конверсиями двух групп существует статистически значимая разница - пользователи, совершающие tips_click, чаще в 1,8 раза просматривают контакты, чем те, кто просто совершает событие tips_show.
Это можно обосновать тем, что пользователь, раз он кликнул на рекламное объявление, уже заинтересовался данным товаром, и, соответственно, с большей вероятностью купит его.
Проверка гипотезы о различии конверсии в просмотр контактов у двух групп пользователей: те, которые совершают действия photos_show, и те, которые не совершают photos_show.¶
- Нулевая гипотеза, Но, - гипотеза о равенстве конверсии между теми, кто совершают
photos_showи теми, кто не просматривает фотографии, в просмотр контактов (contacts_show). - Альтернативная гипотеза, На, - гипотеза о различии в конверсии между этими группами.
- Также, как и в предыдущем пункте, тест - двусторонний, порог статистической значимости
alphaравен 0,05. - Проверка проводится с помощью z-критерия.
# количество пользователей, совершавших `photos_show`, и количество пользователей совершавших так же `contacts_show`
photouser_n = len(user_event_n.query('event_name == "photos_show"'))
target_photouser = (user_event_n
.query('event_name == "photos_show" or event_name == "contacts_show"')
.groupby('user_id', as_index=0).count()
)
target_photouser_n = target_photouser.query('event_name == 2')['user_id'].count()
print('Количество фотоюзеров: ', photouser_n, '\nКоличество целевых фотоюзеров: ', target_photouser_n)
Количество фотоюзеров: 1095 Количество целевых фотоюзеров: 339
# количество пользователей, не совершавших `photos_show`, и количество пользователей совершавших так же `contacts_show`
nonphotouser_n = len(source) - photouser_n
target_nonphotouser_n = user_event_n.query('event_name == "contacts_show"')['user_id'].count() - target_photouser_n
print('Количество нефотоюзеров: ', nonphotouser_n, '\nКоличество целевых нефотоюзеров: ', target_nonphotouser_n)
Количество нефотоюзеров: 3198 Количество целевых нефотоюзеров: 642
Проверяем гипотезу:
z_test(target_photouser_n, photouser_n, target_nonphotouser_n, nonphotouser_n)
p-значение: 1.3278267374516872e-13 Отвергаем нулевую гипотезу
# конверсия групп
print('Конверсия целевой группы: ', 100*target_photouser_n / photouser_n, '%'
, '\nКонверсия нецелевой группы: ', 100*target_nonphotouser_n / nonphotouser_n, '%')
# различие конверсий
print('\nОтношение конверсий: ',(target_photouser_n/photouser_n)/(target_nonphotouser_n/nonphotouser_n))
Конверсия целевой группы: 30.958904109589042 % Конверсия нецелевой группы: 20.075046904315197 % Отношение конверсий: 1.5421584944309308
Различие в конверсии - 1,5 раза, и это статистически значимое различие. Пользователи, просматривающие фотографии в объявлении, в полтора раза чаще интересуются контактами продавца, чем те пользователи, кто не смотрит фотографии.
Пользователь обычно не смотрит фотографии просто так, его интересует предлагаемый или разыскиваемый товар - он сравнивает и выбирает,он присматривает (в том числе и при помощи фотографий), т.е. он уже готов его купить.
Задача проекта - выяснить, как можно улучшить мобильное приложение "Ненужные вещи" на основе анализа данных о поведении пользователей в приложении.
Входные данные представляют собой два датасета с 70000+ событий, совершенных 4000+ пользователями за четыре недели, начиная с 07.10.2019 года.
На этапе загрузки и предобработки данные были проверены на пропуски и дубликаты, было приведено к одному названию событие просмотра контактов, contacts_show, указанное в некоторых ячейках как show_contacts.
На этапе исследовательского анализа мы объединили отдельные события в датафрейме в сессии, чтобы понять, какие последовательности действий выполняет пользователь, перед тем как совершить событие, указанное в задании для проекта, как целевое - contacts_show, просмотр контактов.
На этапе основных вопросов исследования было отобрано четыре наиболее распространенных сценария (последовательности действий), таких, чтобы последним действием являлось целевое. Для них была вычислена конверсия для каждого шага и визуализация в виде воронок конверсии.
Лучшую конверсию из этих четырех показал сценарий photos_show -> contacts_show - 30%. Пользователи с большей вероятностью доберутся до контактов продавца, если будут просматривать больше фотографий.
Худшая конверсия при переходе от показа рекомендаций или от поиска - 20%. Возможно, приложение показывает пользователю не совсем тот товар, который он хотел бы видеть.
Также была построена диаграмма относительной частоты событий для тех пользователей, кто совершил contacts_show, и тех, кто не совершал. Согласно визуализации, пользователи, совершившие contacts_show, чаще просматривают фотографии и кликают на рекомендации. Т.е. выбор продукта происходит, в основном, благодаря его фотографиям.
На этапе проверки гипотез мы проверили методами статистики гипотезы о том, что у пользователей, совершающих действия tips_click и photos_show (две разные гипотезы и проверки), конверсия в целевое действие "просмотр контактов" различается с остальными пользователями. Статистически значимые различия в конверсии для этих двух событий следующие:
tips_click - 1,8 раза;
photos_show - 1,5 раза.
Это является, скорее всего, следствием активного поиска продукта пользователями. Они больше сравнивают по фотографиям, кликают по рекомендациям и, в результате, с большей вероятностью узнают контакты продавца и покупают товар.
Рекомендации
Результаты анализа поведения пользователей в приложении показывают, что для того, чтобы улучшить конверсию в целевое действие и удобство пользования приложением, следует:
- доработать рекомендательные алгоритмы приложения, чтобы более точно предугадать запросы пользователя;
- доработать поисковые алгоритмы приложения, чтобы выводить рекомендации более соответствующие поисковому запросу;
- поэкспериментировать с просмотром фотографий - возможно там еще есть потенциал для увеличения конверсии.